package org.jeecg.modules.shiro.authc;

import cn.com.hhrcw.entity.EnInfo;
import cn.com.hhrcw.entity.PiInfo;
import cn.com.hhrcw.entity.SysUserDepart;
import cn.com.hhrcw.service.IEnInfoService;
import cn.com.hhrcw.service.IPiInfoService;
import cn.com.hhrcw.service.ISysUserDepartService;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.jeecg.common.constant.CacheConstant;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.system.api.ISysBaseAPI;
import org.jeecg.common.system.util.JwtUtil;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.util.RedisUtil;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.shiro.vo.DefContants;
import org.jeecg.modules.system.service.ISysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;

import java.util.Set;

/**
 * @Description: 用户登录鉴权和获取用户授权
 * @Author: Scott
 * @Date: 2019-4-23 8:13
 * @Version: 1.1
 */
@Component
@Slf4j
public class ShiroRealm extends AuthorizingRealm {

    @Value("${fake.token}")
    private String fakeToken;

    @Value("${fake.requiresPermissions}")
    private String fakeRequiresPermissions;

    @Autowired
    @Lazy
    private ISysUserService sysUserService;

    @Autowired
    private IPiInfoService piInfoService;

    @Autowired
    private IEnInfoService enInfoService;

    @Autowired
    private ISysUserDepartService userDepartService;

    @Autowired
    @Lazy
    private ISysBaseAPI sysBaseAPI;

    @Autowired
    @Lazy
    private RedisUtil redisUtil;

    /**
     * 必须重写此方法，不然Shiro会报错
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

    /**
     * 权限信息认证(包括角色以及权限)是用户访问controller的时候才进行验证(redis存储的此处权限信息)
     * 触发检测用户权限时才会调用此方法，例如checkRole,checkPermission
     *
     * @param principals 身份信息
     * @return AuthorizationInfo 权限信息
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        log.info("===============Shiro权限认证开始============ [ roles、permissions]==========");
        String username = null;
        if (principals != null) {
            LoginUser sysUser = (LoginUser) principals.getPrimaryPrincipal();
            username = sysUser.getUsername();
        }
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();

        // 设置用户拥有的角色集合，比如“admin,test”
        Set<String> roleSet = sysUserService.getUserRolesSet(username);
        info.setRoles(roleSet);

        // 设置用户拥有的权限集合，比如“sys:role:add,sys:user:add”
        Set<String> permissionSet = sysUserService.getUserPermissionsSet(username);
        info.addStringPermissions(permissionSet);
        log.info("===============Shiro权限认证成功==============");
        return info;
    }

    /**
     * 用户信息认证是在用户进行登录的时候进行验证(不存redis)
     * 也就是说验证用户输入的账号和密码是否正确，错误抛出异常
     *
     * @param auth 用户登录的账号密码信息
     * @return 返回封装了用户信息的 AuthenticationInfo 实例
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
        JwtToken jwtToken = (JwtToken) auth.getPrincipal();
        if (StrUtil.isBlank(jwtToken.getToken())) {
            String token = JwtUtil.sign(jwtToken.getUsername(), jwtToken.getPassword(), Dict.create().set("platform", jwtToken.getPlatform()));
            jwtToken.setToken(token);
            redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, token);
            //2小时失效
            redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME * 4 / 1000);
        }
        // 校验token有效性
        LoginUser loginUser = this.checkUserTokenIsEffect(jwtToken);
        loginUser.setToken(jwtToken.getToken());
        String phone = loginUser.getPhone().split("@")[0];
        loginUser.setPhone(phone);
        return new SimpleAuthenticationInfo(loginUser, jwtToken.getToken(), getName());
    }

    /**
     * 校验token的有效性
     *
     * @param jwtToken
     */
    public LoginUser checkUserTokenIsEffect(JwtToken jwtToken) throws AuthenticationException {
        String fakeToken = this.fakeToken;
        // 解密获得username，用于和数据库进行对比
        String username = JwtUtil.getUsername(jwtToken.getToken());
        if (username == null) {
            throw new AuthenticationException("非法登陆!");
        }
        // 查询用户信息
        log.debug("———校验token是否有效————checkUserTokenIsEffect——————— " + jwtToken);
        LoginUser loginUser = sysBaseAPI.getUserByPhone(jwtToken.getPhone());
        //当前登陆的平台 0 个人端 1 企业端 2 平台端
        if (jwtToken.getPlatform() == 1 && loginUser == null) {
            cn.com.hhrcw.entity.SysUser sysUser = new cn.com.hhrcw.entity.SysUser();
            sysUser.setSalt(jwtToken.getSalt());
            sysUser.setUsername(jwtToken.getUsername());
            sysUser.setPassword(jwtToken.getPassword());
            sysUser.setPhone(jwtToken.getPhone());
            sysUser.setDelFlag(false);
            sysUser.setStatus(1);
            sysUser.setCreateBy("admin");
            sysUser.setCreateTime(new DateTime());

            PiInfo piInfo = new PiInfo();
            piInfo.setDelFlag(false);
            piInfo.setUsername(sysUser.getUsername());
            piInfo.setPhone(sysUser.getPhone());
            piInfo.setCreateTime(DateTime.now());
            piInfo.setDelFlag(false);
            piInfo.setEnRoleType(0);
            piInfo.setRoleType("1,");
            piInfo.setCreateBy("admin");
            piInfo.setCreateTime(new DateTime());
            piInfoService.createUserInfo(sysUser, piInfo);
            loginUser = new LoginUser();
            BeanUtil.copyProperties(sysUser, loginUser);
        } else if (jwtToken.getPlatform() == 1 && loginUser != null) {
            SysUserDepart depart = userDepartService.lambdaQuery().eq(SysUserDepart::getUserId, loginUser.getId()).one();
            if (depart != null) {
                EnInfo enInfo = this.enInfoService.getById(depart.getDepId());
                loginUser.setCompanyId(enInfo.getId());
                if (!enInfo.getEnabled()) {
                    throw new AuthenticationException("账号已被禁用");
                }
            }
        } else if (loginUser == null) {
            throw new AuthenticationException("账号错误!");
        }

        if (CommonConstant.DEL_FLAG_1.equals(loginUser.getDelFlag())) {
            throw new AuthenticationException("账号已注销");
        }
        //情况3：根据用户信息查询，该用户已冻结
        if (CommonConstant.USER_FREEZE.equals(loginUser.getStatus())) {
            throw new AuthenticationException("账号已被锁定,请联系管理员");
        }

        String token = jwtToken.getToken();
        // 校验token是否超时失效 & 或者账号密码是否错误
        if (!jwtTokenRefresh(jwtToken, username, loginUser.getPassword())) {
            redisUtil.del("ONLINE_" + loginUser.getId());
            throw new AuthenticationException("登陆失效，请重新登录!");
        }
        redisUtil.set("ONLINE_" + loginUser.getId(), token);
        redisUtil.expire("ONLINE_" + loginUser.getId(), JwtUtil.EXPIRE_TIME * 4 / 1000);
        return loginUser;
    }

    /**
     * JWTToken刷新生命周期 （实现： 用户在线操作不掉线功能）
     * 1、登录成功后将用户的JWT生成的Token作为k、v存储到cache缓存里面(这时候k、v值一样)，缓存有效期设置为Jwt有效时间的2倍
     * 2、当该用户再次请求时，通过JWTFilter层层校验之后会进入到doGetAuthenticationInfo进行身份验证
     * 3、当该用户这次请求jwt生成的token值已经超时，但该token对应cache中的k还是存在，则表示该用户一直在操作只是JWT的token失效了，程序会给token对应的k映射的v值重新生成JWTToken并覆盖v值，该缓存生命周期重新计算
     * 4、当该用户这次请求jwt在生成的token值已经超时，并在cache中不存在对应的k，则表示该用户账户空闲超时，返回用户信息已失效，请重新登录。
     * 注意： 前端请求Header中设置Authorization保持不变，校验有效性以缓存中的token为准。
     * 用户过期时间 = Jwt有效时间 * 2。
     *
     * @param userName
     * @param passWord
     * @return
     */
    public boolean jwtTokenRefresh(JwtToken jwtToken, String userName, String passWord) {
        String token = jwtToken.getToken();
        if (!redisUtil.hasKey(CommonConstant.PREFIX_USER_TOKEN + token)) {
            return false;
        }
        String cacheToken = String.valueOf(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN + token));
        if (oConvertUtils.isNotEmpty(cacheToken)) {
            // 校验token有效性
            if (!JwtUtil.verify(cacheToken, userName, passWord)) {
                String newAuthorization = JwtUtil.sign(userName, passWord, Dict.create().set("platform", jwtToken.getPlatform()));
                // 设置超时时间
                redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, newAuthorization);
                redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME * 4 / 1000);
            }
            return true;
        }
        return false;
    }

    /**
     * 清除当前用户的权限认证缓存
     *
     * @param principals 权限信息
     */
    @Override
    public void clearCache(PrincipalCollection principals) {
        super.clearCache(principals);
    }

    /**
     * 禁用权限判断
     * TODO 仅在开发环境使用
     *
     * @param principals
     * @param permission
     * @return
     */
    @Override
    public boolean isPermitted(PrincipalCollection principals, String permission) {

        //2020年11月7日14:09:43 微调星星代码（单独属性判断，估计一期比较暴力，即使正式环境可能不设置按钮级别权限了），放开权限
        boolean aTrue = fakeRequiresPermissions.equalsIgnoreCase("true");
        if (StringUtils.isNotBlank(this.fakeRequiresPermissions) && aTrue) {
            return true;
        }
        return super.isPermitted(principals, permission);
    }

    @Override
    public void onLogout(PrincipalCollection principals) {
        String token = SpringContextUtils.getHttpServletRequest().getHeader(DefContants.X_ACCESS_TOKEN);
        String username = JwtUtil.getUsername(token);
        LoginUser sysUser = sysBaseAPI.getUserByName(username);
        log.info(" 用户名:  " + sysUser.getRealname() + ",退出成功！ ");
        //清空用户登录Token缓存
        redisUtil.del(CommonConstant.PREFIX_USER_TOKEN + token);
        //清空用户登录Shiro权限缓存
        redisUtil.del(CommonConstant.PREFIX_USER_SHIRO_CACHE + sysUser.getId());
        //清空用户的缓存信息（包括部门信息），例如sys:cache:user::<username>
        redisUtil.del(String.format("%s::%s", CacheConstant.SYS_USERS_CACHE, sysUser.getUsername()));
        redisUtil.del("ONLINE_" + sysUser.getId());
        //调用shiro的logout
        super.onLogout(principals);
    }

    @Override
    public CredentialsMatcher getCredentialsMatcher() {
        CredentialsMatcher matcher = (auth, info) -> {
            String token = (String) auth.getCredentials();
            token = String.valueOf(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN + token));
            LoginUser sysUser = (LoginUser) info.getPrincipals().getPrimaryPrincipal();
            return JwtUtil.verify(token, sysUser.getUsername(), sysUser.getPassword());
        };
        return matcher;
    }
}
